/*  ----------------------------------------------------------------------------
 *  Arello Mobile
 *  Fast 3d Engine
 *  ----------------------------------------------------------------------------
 * Except where otherwise noted, this work is licensed under
 * http://creativecommons.org/licenses/by/3.0
 */

#include "Emitter.h"
#include <boost/scoped_ptr.hpp>
#include <resource/Manager.h>

#define _USE_MATH_DEFINES 
#include <math.h>
#include <lib3d/node/Camera.h>
#include <math/utl.h>
#include <xml/Saver.h>

namespace liba
{
	namespace lib3d
	{
		namespace node
		{
			const Atom Emitter::node_name("ParticlesEmitter");
			
			Emitter::Emitter(const Atom & name, Node * parent) : Node(name, parent), _particleSystem(NULL), _particles(NULL), _aliveParticles(0), 
																 _particlesCount(0), _lastEmitTime(0.0f), _lifeTime(0.0f), _paused(false), _needRelease(false)
			{
				node_type = node_name;
				hard::Manager::add_dependent(this);
			}
			
			Emitter::Emitter(const Emitter & other) : Node(other), _particleSystem(other._particleSystem->retain()), _particles(NULL), _aliveParticles(0), 
													  _particlesCount(0), _lastEmitTime(0.0f), _lifeTime(0.0f), _paused(false), _needRelease(false)
			{
				node_type = node_name;
				hard::Manager::add_dependent(this);
				
				// save particles count
				if(_particleSystem != NULL)
				{
					_particlesCount = _particleSystem->get_max_particles_count();
					_lastEmitTime   = math::random(0.0f, _particleSystem->get_emission_rate());
				}
			}
			
			Emitter::~Emitter()
			{
				// remove from hardware dependent list
				hard::Manager::remove_dependent(this);

				// delete particles array
				if(_particles != NULL)
					delete [] _particles;
				
				// release particle system
				if(_particleSystem != NULL)
					_particleSystem->release();
			}
			
			void Emitter::on_add_hardware(hard::Manager::hardware_type * ha)
			{
				hard_vector.push_back(SweetPair(ha, ha->create_vertex_buffer(true)));
			}

			void Emitter::on_save_attributes(xml::SaverBase * saver) const
			{
				Node::on_save_attributes(saver);

				if(_particleSystemPath != "")
					saver->save_attribute("ParticleSystem", _particleSystemPath);

				if(_paused)
					saver->save_attribute("Paused", _paused);
			}

			void Emitter::on_save_nodes(xml::SaverBase * saver) const
			{
				Node::on_save_nodes(saver);

				if(_particleSystemPath == "")
					saver->save_node("ParticleSystem", _particleSystem);
			}
			
			bool Emitter::on_node(xml::Provider * provider, const std::string & name)
			{
				// particle system node
				if(name == "ParticleSystem")
				{
					// create new particle system
					if(_particleSystem != NULL)
						_particleSystem->release();
						
					_particleSystem = new particles::ParticleSystem("");
					
					// parse node
					if(!provider->parse_object(_particleSystem))
						return false;
					
					// save particles count
					_particlesCount = _particleSystem->get_max_particles_count();
					_lastEmitTime   = math::random(0.0f, _particleSystem->get_emission_rate());
					
					if (!_particleSystem->getActivate())
						pause();

					// all ok
					return true;
				}
				
				// delegate to node
				return Node::on_node(provider, name);
			}
			
			bool Emitter::on_attribute(xml::Provider * provider, const std::string & name, const std::string & value)
			{
				// path to particle system
				if(name == "ParticleSystem")
				{
					_particleSystemPath = value;

					// load resouce for particle system
					boost::scoped_ptr<resource::Resource> res(resource::Manager::create_resource(value.c_str()));
					if(!res.get())
						throw GluckBadResource(__FUNCTION__, "Failed to load", value.c_str());
					
					// create new particle system
					if(_particleSystem != NULL)
						_particleSystem->release();
					
					_particleSystem = new particles::ParticleSystem("");
					
					// parse it
					if(!xml::StreamResource(res.get()).load(&xml::Root(_particleSystem, "ParticleSystem")))
						throw GluckBadResource(__FUNCTION__, "Parse failed", value.c_str());
					
					// save particles count
					_particlesCount = _particleSystem->get_max_particles_count();
					_lastEmitTime   = math::random(0.0f, _particleSystem->get_emission_rate());
					if (!_particleSystem->getActivate())
						pause();

					// all ok
					return true;
				}
				
				if(name == "Paused")
					return converter::convert(value, &_paused);
				
				// delegate to node
				return Node::on_attribute(provider, name, value);
			}
			
			Emitter * Emitter::Clone()
			{
				return new Emitter(*this);
			}
			
			void Emitter::set_particle_system(particles::ParticleSystem * particle_system)
			{
				// release old particles system
				if(_particleSystem != NULL)
					_particleSystem->release();
				
				// assign new particle system
				_particleSystem = particle_system != NULL ? particle_system->retain()                  : NULL;
				_particlesCount = _particleSystem != NULL ? _particleSystem->get_max_particles_count() : 0;
				
				// release old particles array
				if(_particles != NULL)
					delete [] _particles;
				
				_particles = NULL;
			}
			
			void Emitter::LifeCycle(time_float delta_time)
			{
				// update lifetime
				if(!_paused)
					_lifeTime += delta_time;
				
				// check if we have associated particle system
				if(_particleSystem == NULL)
					return;

				// check if we need reallocate particles buffer
				if(_particlesCount != _particleSystem->get_max_particles_count())
				{
					// reset particles count
					_particlesCount = _particleSystem->get_max_particles_count();

					// release old particles array
					if(_particles != NULL)
						delete [] _particles;

					_particles = NULL;
				}

				// getting emitter matrix
				Matrix emitterMatrix = GetGlobalMatrix();

				// validate emitter life time
				if((_lifeTime < _particleSystem->get_emitter_lifetime() || _particleSystem->get_emitter_lifetime() == 0) && !_needRelease && _paused == false)
				{
					// check if we need initialize particles array
					if(_particles == NULL)
						_particles = new particles::Particle[_particlesCount];
					
					// check if we need spawn new particles
					if(_lastEmitTime + delta_time >= _particleSystem->get_emission_rate())
					{
						// compute emission particles count
						size_t emissionCount = 0;
						float emissionOffset = 0.0f;
						if(_particleSystem->get_emission_rate() > 0.0f)
						{
							emissionCount  = int((_lastEmitTime + delta_time) / _particleSystem->get_emission_rate()) * _particleSystem->get_emission_count();
							emissionOffset = fmodf(_lastEmitTime + delta_time, _particleSystem->get_emission_rate());
						}
						else
						{
							emissionCount = _particleSystem->get_emission_count();
						}

						// emit required number of particles
						size_t emitted = 0;
						for(size_t i = 0; i < _particlesCount && emitted != emissionCount; ++i)
						{
							// if here is dead particle
							if(!_particles[i].isAlive)
							{
								// one more emitted
								emitted++;
								callBack = true;
								
								// initialize particle
								_particleSystem->initialize_particle(&_particles[i], emitterMatrix);
							}
							else
							{
								callBack = false;
							}
						}
						
						// update emit time
						_lastEmitTime = emissionOffset;
					}
					else
					{
						_lastEmitTime += delta_time;
					}
				}
				
				// update all alive particles
				if(_particleSystem->getActivate() && _particles != NULL)
					_aliveParticles = _particleSystem->update_particles(_particles, _particlesCount, delta_time);
				
				// check if we need kill emitter
				if(((_lifeTime >= _particleSystem->get_emitter_lifetime() && _particleSystem->get_emitter_lifetime() != 0) || _needRelease) && _aliveParticles == 0)
					mark_as_dead();
			}
			
			void Emitter::update_buffer(hard::RenderList * render_list)
			{
				// check if we have particles array to render
				if(_particles == NULL || _aliveParticles == 0)
					return;
				
				// allocate array for the new vertices
				SimpleArray<float> vertexPositions(_aliveParticles * 6 * 3);
				SimpleArray<float> vertexColors(_aliveParticles * 6 * 4);
				SimpleArray<float> vertexUVs(_aliveParticles * 6 * 2);
				
				// getting pointers to vertices data
				float * positionData = vertexPositions.data();
				float * colorData    = vertexColors.data();
				float * uvData       = vertexUVs.data();
				
				// compute camera-align matrix
				// getting camera's world matrix
				Matrix cameraWorld = render_list->camera_world;
				
				// compute full world matrix
				Matrix fullWorld = Matrix::invert(cameraWorld) * GetGlobalMatrix();
				
				// remove translation from matrix
				fullWorld.rc(0, 3) = 0.0f;
				fullWorld.rc(1, 3) = 0.0f;
				fullWorld.rc(2, 3) = 0.0f;
				
				// compute final align matrix
				Matrix alignMatrix = Matrix::invert(fullWorld);
				
				// particle values
				math::Vector2<Float> halfSize;
				math::Vector2<Float> minCorner;
				math::Vector2<Float> maxCorner;
				Color                color;
				float                alpha;
				Vector               center;
				Matrix               rotate;
				Vector               position;
				bool                 orthoCamera   = Camera::active_camera()->ortho_projection();
				bool                 needTransform = false;
				
				// fill arrays
				for(size_t i = 0; i < _particlesCount; ++i)
				{
					if(_particles[i].isAlive)
					{
						// getting particle data
						halfSize = _particles[i].currentState.scale / 2.0f;
						color    = _particles[i].currentState.color;
						alpha    = _particles[i].currentState.alpha;
						center   = _particles[i].currentState.position;
						if(!orthoCamera)
						{
							rotate        = _particles[i].currentState.angle != 0 ? alignMatrix * Matrix::rotate(Vector(0.0f, 0.0f, _particles[i].currentState.angle / 180.0f * M_PI)) : alignMatrix;
							needTransform = true;
						}
						else if(_particles[i].currentState.angle != 0)
						{
							rotate        = Matrix::rotate(Vector(0.0f, 0.0f, _particles[i].currentState.angle / 180.0f * M_PI));
							needTransform = true;
						}
						else
						{
							needTransform = false;
						}
						
						// getting texture coords for this particle
						_particleSystem->get_texture_coords_for_time(_particles[i].lifeTime, &minCorner, &maxCorner);

						// compute corners offset
						Vector firstCorner  = needTransform ? rotate * Vector(-halfSize.x, -halfSize.y, 0.0f) : Vector(-halfSize.x, -halfSize.y, 0.0f);
						Vector secondCorner = needTransform ? rotate * Vector(-halfSize.x,  halfSize.y, 0.0f) : Vector(-halfSize.x,  halfSize.y, 0.0f);
						Vector thirdCorner  = needTransform ? rotate * Vector( halfSize.x,  halfSize.y, 0.0f) : Vector( halfSize.x,  halfSize.y, 0.0f);
						Vector fourthCorner = needTransform ? rotate * Vector( halfSize.x, -halfSize.y, 0.0f) : Vector( halfSize.x, -halfSize.y, 0.0f);
						
						// vertex #1
						position        = center + firstCorner;
						*positionData++ = position.x;
						*positionData++ = position.y;
						*positionData++ = position.z;
						*colorData++    = color.r;
						*colorData++    = color.g;
						*colorData++    = color.b;
						*colorData++    = alpha;
						*uvData++       = minCorner.x;
						*uvData++       = minCorner.y;
						
						// vertex #2
						position        = center + secondCorner;
						*positionData++ = position.x;
						*positionData++ = position.y;
						*positionData++ = position.z;
						*colorData++    = color.r;
						*colorData++    = color.g;
						*colorData++    = color.b;
						*colorData++    = alpha;
						*uvData++       = minCorner.x;
						*uvData++       = maxCorner.y;
						
						// vertex #3
						position        = center + thirdCorner;
						*positionData++ = position.x;
						*positionData++ = position.y;
						*positionData++ = position.z;
						*colorData++    = color.r;
						*colorData++    = color.g;
						*colorData++    = color.b;
						*colorData++    = alpha;
						*uvData++       = maxCorner.x;
						*uvData++       = maxCorner.y;
						
						// vertex #4
						position        = center + thirdCorner;
						*positionData++ = position.x;
						*positionData++ = position.y;
						*positionData++ = position.z;
						*colorData++    = color.r;
						*colorData++    = color.g;
						*colorData++    = color.b;
						*colorData++    = alpha;
						*uvData++       = maxCorner.x;
						*uvData++       = maxCorner.y;
						
						// vertex #5
						position        = center + fourthCorner;
						*positionData++ = position.x;
						*positionData++ = position.y;
						*positionData++ = position.z;
						*colorData++    = color.r;
						*colorData++    = color.g;
						*colorData++    = color.b;
						*colorData++    = alpha;
						*uvData++       = maxCorner.x;
						*uvData++       = minCorner.y;
						
						// vertex #6
						position        = center + firstCorner;
						*positionData++ = position.x;
						*positionData++ = position.y;
						*positionData++ = position.z;
						*colorData++    = color.r;
						*colorData++    = color.g;
						*colorData++    = color.b;
						*colorData++    = alpha;
						*uvData++       = minCorner.x;
						*uvData++       = minCorner.y;
					}
				}
				
				// empty float array
				SimpleArray<float> emptyFloat;
				
				// iterate over all hardwares
				HardVector::iterator itr = hard_vector.begin();
				for(; itr != hard_vector.end(); ++itr)
				{
					if(!itr->second->update(vertexPositions, emptyFloat, vertexColors, vertexUVs, emptyFloat))
						THROW(Gluck(__FUNCTION__, "VertexBuffer::update() failed"));
				}
			}
			
			void Emitter::RenderToList(hard::Hardware * hardware, hard::RenderList * render_list)
			{
				// check if we have particles to render
				if(_particles == NULL || _aliveParticles == 0)
					return;
				
				// find data for this hardware
				hard::VertexBuffer * vb = find_data(hardware);
				if(vb == NULL)
					return;
				
				// check if texture already loaded
				hard::Texture * texture = _particleSystem->get_particles_texture(hardware);
				if(texture == NULL)
					return;
				
				// update vertex buffer
				update_buffer(render_list);
				
				// fill render model structure
				hard::RenderModel renderModel;
				
                if (_particleSystem->is_parented_to_emitter())
                    renderModel.world = GetGlobalMatrix();
                else
                    renderModel.world = Matrix::identity();
				renderModel.vb                                      = vb;
				renderModel.primitive_start                         = 0;
				renderModel.primitive_count                         = _aliveParticles * 2;
				renderModel.ib                                      = NULL;
				renderModel.vertex_start                            = 0;
				renderModel.vertex_count                            = _aliveParticles * 6;
				renderModel.material.emissive_texture.texture       = texture;
				renderModel.material.emissive_texture.wrap          = false;
				renderModel.material.emissive_texture.alpha_channel = true;
				renderModel.material.transparency                   = 0.001f;
				renderModel.blendMode								= _particleSystem->get_blend_mode();
				
				// add to render list
				render_list->models.push_back(renderModel);
			}
			
			void Emitter::pause()
			{
				_paused = true;
			}
			
			void Emitter::resume()
			{
 				_paused = false;
			}
			
			bool Emitter::is_paused()
			{
				return _paused;
			}
			
			void Emitter::release()
			{
				_needRelease = true;
			}

			void Emitter::activateParticleSystem(bool _active) 
			{
				if (!_active)
				{
					_particleSystem->deactivateParticleSystem(_active);
					pause();
					return;
				}
				resume();
				_particleSystem->activateParticleSystem(_active);
			}
		}
	}
}